reqs

python==3.13.0
pandas==2.2.3
notebook==7.3.3
numpy==1.26.4
scikit-learn==1.6.1
sweetviz==2.3.1
lightgbm==4.6.0
optuna-integration[sklearn]==4.2.1
shap==0.47.1
matplotlib==3.10.1
seaborn==0.13.2

Загружаем данные¶

Рассмотрим данные из соревнования по предсказанию стоимости жилья

In [1]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import shap
import sklearn
import sweetviz as sv
from sklearn.model_selection import train_test_split
import lightgbm
import optuna
from optuna.distributions import IntDistribution, FloatDistribution, CategoricalDistribution
from sklearn.pipeline import Pipeline
from sklearn.model_selection import cross_val_score
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
import random
In [2]:
import warnings
# Фильтруем маловажные предупреждения
warnings.filterwarnings("ignore", category=UserWarning, 
                       module="sklearn.preprocessing._encoders")
warnings.filterwarnings("ignore", category=UserWarning,
                       module="sklearn.utils.validation")
In [3]:
# !wget  -O 'train.csv' -q 'https://www.dropbox.com/s/6dxq90t0prn2vaw/_train_sem2.csv?dl=0'
# Изначальный код загрузки не сработал, датасет загружен вручную
In [4]:
data = pd.read_csv("train.csv")
data.head()
Out[4]:
Id MSSubClass MSZoning LotFrontage LotArea Street Alley LotShape LandContour Utilities ... PoolArea PoolQC Fence MiscFeature MiscVal MoSold YrSold SaleType SaleCondition SalePrice
0 1 60 RL 65.0 8450 Pave NaN Reg Lvl AllPub ... 0 NaN NaN NaN 0 2 2008 WD Normal 208500
1 2 20 RL 80.0 9600 Pave NaN Reg Lvl AllPub ... 0 NaN NaN NaN 0 5 2007 WD Normal 181500
2 3 60 RL 68.0 11250 Pave NaN IR1 Lvl AllPub ... 0 NaN NaN NaN 0 9 2008 WD Normal 223500
3 4 70 RL 60.0 9550 Pave NaN IR1 Lvl AllPub ... 0 NaN NaN NaN 0 2 2006 WD Abnorml 140000
4 5 60 RL 84.0 14260 Pave NaN IR1 Lvl AllPub ... 0 NaN NaN NaN 0 12 2008 WD Normal 250000

5 rows × 81 columns

Первое, что стоит заметить — у нас в данных есть уникальное для каждого объекта поле id. Обычно такие поля только мешают и способствуют переобучению. Удалим это поле из данных.

Разделим данные на обучающую и тестовую выборки. Для простоты не будем выделять дополнительно валидационную выборку (хотя это обычно стоит делать, она нужна для подбора гиперпараметров модели, то есть параметров, которые нельзя подбирать по обучающей выборке). Дополнительно нам придется отделить значения целевой переменной от данных.

Вопрос 1: Почему поля типа id могут вызвать переобучение модели (не обязательно линейной)?

Ответ: Id не несёт в себе никакой полезной информации. Простые модели будут пытаться найти взаимосвязи, которых нет. А сложные могут попытаться связать id с целевой переменной, что приведёт к полному переобучению.

Вопрос 2: Почему стоит дополнительно отделять валидационную выборку?

Ответ: Валидационная выборка - единственный способ получить промежуточную оценку во время обучения или подбора гиперпараметров. Тестовые данные для этого не подходят, они должны оставаться эталонно-нетронутыми до финального теста, ведь именно на них мы проверяем модель на степень переобучения и общую адекватность.

Вопрос 3: Обратите внимание на фиксацию random_state при сплите данных. Почему это важно?

Ответ: Фиксация рандомности разделения. Это гарантирует что при повторном запуске кода данные разделятся так-же как и в предыдущие разы. То-же относится к некоторым моделям, предусматривающим рандомизацию в расчётах. Без константы случайности результаты экспериментов будут невоспроизводимыми.
In [5]:
# Константа случайности
RANDOM_STATE = 10

Задание¶

    1. Проведите EDA анализ данных
    1. Выберите наиболее релевантные факторы для прогноза
    1. Подберите лучшую модель (используйте в качестве метрики качества показатель RMSE
    1. Проверьте качество данной модели - на сколько она точна, насколько стабильна, насколько правильно специфирована

Разведочный анализ данных и предобработка¶

Начнём с базового ознакомления с данными. Типы, количество, названия признаков - нет ничего надёжнее .info()

In [6]:
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1460 entries, 0 to 1459
Data columns (total 81 columns):
 #   Column         Non-Null Count  Dtype  
---  ------         --------------  -----  
 0   Id             1460 non-null   int64  
 1   MSSubClass     1460 non-null   int64  
 2   MSZoning       1460 non-null   object 
 3   LotFrontage    1201 non-null   float64
 4   LotArea        1460 non-null   int64  
 5   Street         1460 non-null   object 
 6   Alley          91 non-null     object 
 7   LotShape       1460 non-null   object 
 8   LandContour    1460 non-null   object 
 9   Utilities      1460 non-null   object 
 10  LotConfig      1460 non-null   object 
 11  LandSlope      1460 non-null   object 
 12  Neighborhood   1460 non-null   object 
 13  Condition1     1460 non-null   object 
 14  Condition2     1460 non-null   object 
 15  BldgType       1460 non-null   object 
 16  HouseStyle     1460 non-null   object 
 17  OverallQual    1460 non-null   int64  
 18  OverallCond    1460 non-null   int64  
 19  YearBuilt      1460 non-null   int64  
 20  YearRemodAdd   1460 non-null   int64  
 21  RoofStyle      1460 non-null   object 
 22  RoofMatl       1460 non-null   object 
 23  Exterior1st    1460 non-null   object 
 24  Exterior2nd    1460 non-null   object 
 25  MasVnrType     588 non-null    object 
 26  MasVnrArea     1452 non-null   float64
 27  ExterQual      1460 non-null   object 
 28  ExterCond      1460 non-null   object 
 29  Foundation     1460 non-null   object 
 30  BsmtQual       1423 non-null   object 
 31  BsmtCond       1423 non-null   object 
 32  BsmtExposure   1422 non-null   object 
 33  BsmtFinType1   1423 non-null   object 
 34  BsmtFinSF1     1460 non-null   int64  
 35  BsmtFinType2   1422 non-null   object 
 36  BsmtFinSF2     1460 non-null   int64  
 37  BsmtUnfSF      1460 non-null   int64  
 38  TotalBsmtSF    1460 non-null   int64  
 39  Heating        1460 non-null   object 
 40  HeatingQC      1460 non-null   object 
 41  CentralAir     1460 non-null   object 
 42  Electrical     1459 non-null   object 
 43  1stFlrSF       1460 non-null   int64  
 44  2ndFlrSF       1460 non-null   int64  
 45  LowQualFinSF   1460 non-null   int64  
 46  GrLivArea      1460 non-null   int64  
 47  BsmtFullBath   1460 non-null   int64  
 48  BsmtHalfBath   1460 non-null   int64  
 49  FullBath       1460 non-null   int64  
 50  HalfBath       1460 non-null   int64  
 51  BedroomAbvGr   1460 non-null   int64  
 52  KitchenAbvGr   1460 non-null   int64  
 53  KitchenQual    1460 non-null   object 
 54  TotRmsAbvGrd   1460 non-null   int64  
 55  Functional     1460 non-null   object 
 56  Fireplaces     1460 non-null   int64  
 57  FireplaceQu    770 non-null    object 
 58  GarageType     1379 non-null   object 
 59  GarageYrBlt    1379 non-null   float64
 60  GarageFinish   1379 non-null   object 
 61  GarageCars     1460 non-null   int64  
 62  GarageArea     1460 non-null   int64  
 63  GarageQual     1379 non-null   object 
 64  GarageCond     1379 non-null   object 
 65  PavedDrive     1460 non-null   object 
 66  WoodDeckSF     1460 non-null   int64  
 67  OpenPorchSF    1460 non-null   int64  
 68  EnclosedPorch  1460 non-null   int64  
 69  3SsnPorch      1460 non-null   int64  
 70  ScreenPorch    1460 non-null   int64  
 71  PoolArea       1460 non-null   int64  
 72  PoolQC         7 non-null      object 
 73  Fence          281 non-null    object 
 74  MiscFeature    54 non-null     object 
 75  MiscVal        1460 non-null   int64  
 76  MoSold         1460 non-null   int64  
 77  YrSold         1460 non-null   int64  
 78  SaleType       1460 non-null   object 
 79  SaleCondition  1460 non-null   object 
 80  SalePrice      1460 non-null   int64  
dtypes: float64(3), int64(35), object(43)
memory usage: 924.0+ KB

80 признаков, 1460 записей. Сразу видно наличие пропусков. Проведём более глубокий EDA автоматизированными инструментами.

In [7]:
sv.analyze(data, target_feat='SalePrice').show_notebook()
                                             |                                             | [  0%]   00:00 ->…

80 признаков - это больше, чем нам необходимо. Воспользуемся матрицей ассоциаций, чтобы выделить из них наименее информативные, и удалим их.

In [8]:
# data = data.loc[:, data.columns.isin(["SalePrice", "Neighborhood", "OverallQual", "YearBuilt", "YearRemodAdd", "Exterior1st", "Exterior2nd", "MasVnrType", "MasVnrArea", "ExterQual", "Foundation", "BsmtQual", "BsmtFinType1", "TotalBsmtSF", "HeatingQC", "1stFlrSF", "GrLivArea", "FullBath", "KitchenQual", "TotRmsAbvGrd", "Fireplaces", "FireplaceQu", "GarageType", "GarageYrBlt", "GarageFinish", "GarageCars", "GarageArea"])]
# Убираем все признаки, кроме наиболее информативных(Неэффективно. Низкие оценки на кросс-валидации)
data = data.drop(["YrSold", "MiscVal", "MiscFeature", "ScreenPorch", "3SsnPorch", "EnclosedPorch", "Functional", "BsmtHalfBath", "LowQualFinSF", "Heating", "BsmtFinSF2", "ExterCond", "LandSlope", "Utilities", "Street", "MSSubClass", "Id"], axis=1)
# Уберём наименее информативные признаки

y = data["SalePrice"] # целевая переменная (target)
X = data.drop(columns=["SalePrice"]) # из пространства признаков выкинули целевую переменную

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=RANDOM_STATE)

Теперь займёмся препроцессингом. Для ускорения работы с данными сделаем это в пайплайне. Воспользуемся ColumnTransformer, чтобы распределить данные на скейлинг и энкодинг. Попутно заполним отсутствующие значения, у численных данных медианой, у категорий самым часто-встречающимся значением

In [9]:
preprocessor = sklearn.compose.ColumnTransformer([
    ('ohe', Pipeline([
        ('impute', sklearn.impute.SimpleImputer(strategy='most_frequent')),
        ('encode', sklearn.preprocessing.OneHotEncoder(drop='first', handle_unknown='ignore', sparse_output=False))
    ]), \
     X_train.select_dtypes(exclude='number').columns
    ),
    ('scaler', Pipeline([
        ('impute', sklearn.impute.SimpleImputer(strategy='median')),
        ('scale', sklearn.preprocessing.StandardScaler())
    ]), \
     X_train.select_dtypes(include='number').columns
    )
],
remainder='passthrough'
)

Итого, в рамках разведочного анализа и предобработки, мы:

  • Ознакомились с признаками, выявили наличие пропусков и отсутствие ярких аномалий;
  • Выявили и удалили малоинформативные признаки при помощи матрицы ассоциаций;
  • Провели разделение данных на тренировочную и тестовую выборки
  • Подготовили препроцессор для будущих пайплайнов модели, включающий - заполнение пропусков, скейлинг и кодирование признаков.

Обучение моделей¶

Теперь приступим к обучению моделей. Выбирая кандидатов постараемся уделить внимание моделям разнообразной сложности. Никогда не знаешь, когда линейная регрессия покажет себя лучше нейросети.
За представителя сравнительно простой модели возьмём метод опорных векторов с линейным ядром. Обучать будем сразу с подбором гиперпараметров и кросс-валидацией. За метрику возьмём корень среднего квадрата ошибки

In [10]:
svm = Pipeline([
    ('preprocess', preprocessor),
    ('model', sklearn.svm.SVR(kernel='linear'))
])

param_distribution_svm = {
    "model__C": FloatDistribution(1e-3, 1e3, log=True),
    "model__gamma": FloatDistribution(1e-4, 1e1, log=True),
    "model__epsilon": FloatDistribution(0.01, 0.2)
}

svm_cv = optuna.integration.OptunaSearchCV(svm, param_distributions=param_distribution_svm, cv=5, scoring='neg_root_mean_squared_error', random_state=RANDOM_STATE, n_trials=10)
svm_cv.fit(X_train, y_train)
print('RMSE:',cross_val_score(svm_cv.best_estimator_, X_train, y_train, scoring='neg_root_mean_squared_error').mean()*-1)
C:\Users\m8913\AppData\Local\Temp\ipykernel_2812\584707306.py:12: ExperimentalWarning: OptunaSearchCV is experimental (supported from v0.17.0). The interface can change in the future.
  svm_cv = optuna.integration.OptunaSearchCV(svm, param_distributions=param_distribution_svm, cv=5, scoring='neg_root_mean_squared_error', random_state=RANDOM_STATE, n_trials=10)
[I 2025-03-27 11:54:47,738] A new study created in memory with name: no-name-1aed3424-c180-406b-aae1-e98abea44bbf
[I 2025-03-27 11:54:48,146] Trial 0 finished with value: -81888.26082198271 and parameters: {'model__C': 0.003585583615585772, 'model__gamma': 1.0476834405905207, 'model__epsilon': 0.06961979194105183}. Best is trial 0 with value: -81888.26082198271.
[I 2025-03-27 11:54:48,534] Trial 1 finished with value: -81802.6808220816 and parameters: {'model__C': 0.024412890980453777, 'model__gamma': 0.001593717567647103, 'model__epsilon': 0.08915874944020485}. Best is trial 1 with value: -81802.6808220816.
[I 2025-03-27 11:54:48,934] Trial 2 finished with value: -41779.82353450205 and parameters: {'model__C': 38.76749114249326, 'model__gamma': 1.56933184557578, 'model__epsilon': 0.055994602251620215}. Best is trial 2 with value: -41779.82353450205.
[I 2025-03-27 11:54:49,344] Trial 3 finished with value: -39294.46044221624 and parameters: {'model__C': 69.43953913944551, 'model__gamma': 8.048026506705487, 'model__epsilon': 0.01659496624572367}. Best is trial 3 with value: -39294.46044221624.
[I 2025-03-27 11:54:49,728] Trial 4 finished with value: -81822.4227480425 and parameters: {'model__C': 0.019682192940159417, 'model__gamma': 0.00043463729875164016, 'model__epsilon': 0.08245164838984852}. Best is trial 3 with value: -39294.46044221624.
[I 2025-03-27 11:54:50,147] Trial 5 finished with value: -37164.61725644615 and parameters: {'model__C': 152.44208781402085, 'model__gamma': 0.8641843670856602, 'model__epsilon': 0.04089259098446621}. Best is trial 5 with value: -37164.61725644615.
[I 2025-03-27 11:54:50,531] Trial 6 finished with value: -79841.13341517554 and parameters: {'model__C': 0.46476358312751725, 'model__gamma': 0.05924000380710417, 'model__epsilon': 0.11557320544596486}. Best is trial 5 with value: -37164.61725644615.
[I 2025-03-27 11:54:50,924] Trial 7 finished with value: -41776.58068211471 and parameters: {'model__C': 38.7932306032779, 'model__gamma': 0.0032733675353925854, 'model__epsilon': 0.13195667074229078}. Best is trial 5 with value: -37164.61725644615.
[I 2025-03-27 11:54:51,308] Trial 8 finished with value: -58007.32705242101 and parameters: {'model__C': 7.296970093622444, 'model__gamma': 0.007895922728940888, 'model__epsilon': 0.10825492480306731}. Best is trial 5 with value: -37164.61725644615.
[I 2025-03-27 11:54:51,697] Trial 9 finished with value: -45634.59497420418 and parameters: {'model__C': 21.51898696926201, 'model__gamma': 0.1207534873981832, 'model__epsilon': 0.07538472013223435}. Best is trial 5 with value: -37164.61725644615.
RMSE: 37164.61725644615

Этот результат не предел совершенства, поэтому мы попробуем его улучшить.

Воспользуемся более сложной моделью - лёгким градиентным бустингом. Точно так-же подберём гиперпараметры и проверим качество лучшей модели на кросс-валидации.

In [11]:
lgbm = Pipeline([
    ('preprocess', preprocessor),
    ('model', lightgbm.LGBMRegressor(random_state=RANDOM_STATE, verbose=-1, objective='poisson', boosting_type='gbdt'))
])

param_distribution_lgbm = {
    'model__num_leaves': IntDistribution(low=20, high=150, step=1),
    'model__max_depth': IntDistribution(low=3, high=15, step=1),
    'model__learning_rate': FloatDistribution(low=0.01, high=0.3, log=False),
    'model__n_estimators': IntDistribution(low=50, high=500, step=50),
    'model__min_child_samples': IntDistribution(low=5, high=100, step=5),
    'model__colsample_bytree': FloatDistribution(low=0.6, high=1.0),
    'model__reg_alpha': FloatDistribution(low=0, high=1),
    'model__reg_lambda': FloatDistribution(low=0, high=1)
}

lgbm_cv = optuna.integration.OptunaSearchCV(lgbm, param_distributions=param_distribution_lgbm, cv=5, scoring='neg_root_mean_squared_error', random_state=RANDOM_STATE, n_trials=10)

lgbm_cv.fit(X_train, y_train)
print('RMSE:', cross_val_score(lgbm_cv.best_estimator_, X_train, y_train, scoring='neg_root_mean_squared_error').mean()*-1)
C:\Users\m8913\AppData\Local\Temp\ipykernel_2812\2730049852.py:17: ExperimentalWarning: OptunaSearchCV is experimental (supported from v0.17.0). The interface can change in the future.
  lgbm_cv = optuna.integration.OptunaSearchCV(lgbm, param_distributions=param_distribution_lgbm, cv=5, scoring='neg_root_mean_squared_error', random_state=RANDOM_STATE, n_trials=10)
[I 2025-03-27 11:54:52,208] A new study created in memory with name: no-name-d6b4a832-6f45-4e2e-945e-7dd9a41f4e17
[I 2025-03-27 11:54:53,833] Trial 0 finished with value: -28587.832084951377 and parameters: {'model__num_leaves': 32, 'model__max_depth': 13, 'model__learning_rate': 0.10099862980476332, 'model__n_estimators': 150, 'model__min_child_samples': 25, 'model__colsample_bytree': 0.7666499988214839, 'model__reg_alpha': 0.7647446160554672, 'model__reg_lambda': 0.8391429575464384}. Best is trial 0 with value: -28587.832084951377.
[I 2025-03-27 11:54:54,503] Trial 1 finished with value: -29219.8913615595 and parameters: {'model__num_leaves': 51, 'model__max_depth': 13, 'model__learning_rate': 0.29452998508931827, 'model__n_estimators': 50, 'model__min_child_samples': 25, 'model__colsample_bytree': 0.6510501594333025, 'model__reg_alpha': 0.38132446520972907, 'model__reg_lambda': 0.863850814690767}. Best is trial 0 with value: -28587.832084951377.
[I 2025-03-27 11:54:55,650] Trial 2 finished with value: -30877.194067645927 and parameters: {'model__num_leaves': 123, 'model__max_depth': 5, 'model__learning_rate': 0.13891621774473858, 'model__n_estimators': 300, 'model__min_child_samples': 60, 'model__colsample_bytree': 0.9059170632043324, 'model__reg_alpha': 0.30299895417076916, 'model__reg_lambda': 0.6418772144331093}. Best is trial 0 with value: -28587.832084951377.
[I 2025-03-27 11:54:57,423] Trial 3 finished with value: -31027.061265321106 and parameters: {'model__num_leaves': 104, 'model__max_depth': 7, 'model__learning_rate': 0.15996804312047117, 'model__n_estimators': 400, 'model__min_child_samples': 65, 'model__colsample_bytree': 0.7376520423836512, 'model__reg_alpha': 0.1703168406424409, 'model__reg_lambda': 0.8387758324726942}. Best is trial 0 with value: -28587.832084951377.
[I 2025-03-27 11:54:58,743] Trial 4 finished with value: -30456.48701006016 and parameters: {'model__num_leaves': 38, 'model__max_depth': 5, 'model__learning_rate': 0.14171292000913205, 'model__n_estimators': 350, 'model__min_child_samples': 55, 'model__colsample_bytree': 0.8754220091967231, 'model__reg_alpha': 0.5712981963943911, 'model__reg_lambda': 0.8074201583678433}. Best is trial 0 with value: -28587.832084951377.
[I 2025-03-27 11:55:00,401] Trial 5 finished with value: -27320.849053227634 and parameters: {'model__num_leaves': 133, 'model__max_depth': 5, 'model__learning_rate': 0.08794560715288294, 'model__n_estimators': 400, 'model__min_child_samples': 30, 'model__colsample_bytree': 0.6689929656509956, 'model__reg_alpha': 0.9148078283231919, 'model__reg_lambda': 0.3579902570947734}. Best is trial 5 with value: -27320.849053227634.
[I 2025-03-27 11:55:01,314] Trial 6 finished with value: -32061.252342346103 and parameters: {'model__num_leaves': 72, 'model__max_depth': 11, 'model__learning_rate': 0.0674612201494978, 'model__n_estimators': 250, 'model__min_child_samples': 100, 'model__colsample_bytree': 0.7133797847536105, 'model__reg_alpha': 0.9694825133296966, 'model__reg_lambda': 0.2744572662383924}. Best is trial 5 with value: -27320.849053227634.
[I 2025-03-27 11:55:01,804] Trial 7 finished with value: -30662.55262974088 and parameters: {'model__num_leaves': 24, 'model__max_depth': 9, 'model__learning_rate': 0.23520434115270308, 'model__n_estimators': 50, 'model__min_child_samples': 45, 'model__colsample_bytree': 0.950913157219048, 'model__reg_alpha': 0.10602220644538718, 'model__reg_lambda': 0.5953372470432962}. Best is trial 5 with value: -27320.849053227634.
[I 2025-03-27 11:55:04,737] Trial 8 finished with value: -27870.369885998673 and parameters: {'model__num_leaves': 31, 'model__max_depth': 9, 'model__learning_rate': 0.2832730822994129, 'model__n_estimators': 400, 'model__min_child_samples': 25, 'model__colsample_bytree': 0.6554401204582899, 'model__reg_alpha': 0.928746315106096, 'model__reg_lambda': 0.07568300910027659}. Best is trial 5 with value: -27320.849053227634.
[I 2025-03-27 11:55:05,612] Trial 9 finished with value: -34309.37470366442 and parameters: {'model__num_leaves': 76, 'model__max_depth': 14, 'model__learning_rate': 0.0810279534911221, 'model__n_estimators': 50, 'model__min_child_samples': 20, 'model__colsample_bytree': 0.6242388416157856, 'model__reg_alpha': 0.5766428589497673, 'model__reg_lambda': 0.5484028646309496}. Best is trial 5 with value: -27320.849053227634.
RMSE: 27320.849053227634

Значительно лучше, но есть куда совершенствоваться.

Следующая модель - случайный лес.

In [12]:
forest = Pipeline([
    ('preprocess', preprocessor),
    ('model', sklearn.ensemble.RandomForestRegressor(random_state=RANDOM_STATE, n_jobs=-1))
])

param_distribution_forest = {
    "model__n_estimators": IntDistribution(50, 500, step=50),
    "model__max_depth": IntDistribution(3, 15),
    "model__min_samples_split": IntDistribution(2, 20),
    "model__min_samples_leaf": IntDistribution(1, 10),
    "model__criterion": CategoricalDistribution(["squared_error", "absolute_error", "friedman_mse"]),
    "model__max_features": CategoricalDistribution(["sqrt", "log2", None]),
    "model__min_impurity_decrease": FloatDistribution(0.0, 0.5, step=0.01),
    "model__max_samples": FloatDistribution(0.5, 1.0)
}

forest_cv = optuna.integration.OptunaSearchCV(forest, param_distributions=param_distribution_forest, cv=5, scoring='neg_root_mean_squared_error', random_state=RANDOM_STATE, n_trials=10)

forest_cv.fit(X_train, y_train)
print('RMSE:',cross_val_score(forest_cv.best_estimator_, X_train, y_train, scoring='neg_root_mean_squared_error').mean()*-1)
C:\Users\m8913\AppData\Local\Temp\ipykernel_2812\191560939.py:17: ExperimentalWarning: OptunaSearchCV is experimental (supported from v0.17.0). The interface can change in the future.
  forest_cv = optuna.integration.OptunaSearchCV(forest, param_distributions=param_distribution_forest, cv=5, scoring='neg_root_mean_squared_error', random_state=RANDOM_STATE, n_trials=10)
[I 2025-03-27 11:55:07,708] A new study created in memory with name: no-name-f5c5e1e7-5162-4ce2-b45a-32e326e9e095
[I 2025-03-27 11:55:08,464] Trial 0 finished with value: -36120.24060736828 and parameters: {'model__n_estimators': 50, 'model__max_depth': 13, 'model__min_samples_split': 7, 'model__min_samples_leaf': 3, 'model__criterion': 'friedman_mse', 'model__max_features': 'sqrt', 'model__min_impurity_decrease': 0.5, 'model__max_samples': 0.5173551743308518}. Best is trial 0 with value: -36120.24060736828.
[I 2025-03-27 11:55:09,691] Trial 1 finished with value: -36811.91872406435 and parameters: {'model__n_estimators': 150, 'model__max_depth': 4, 'model__min_samples_split': 9, 'model__min_samples_leaf': 9, 'model__criterion': 'squared_error', 'model__max_features': None, 'model__min_impurity_decrease': 0.15, 'model__max_samples': 0.8209386072165546}. Best is trial 0 with value: -36120.24060736828.
[I 2025-03-27 11:55:11,979] Trial 2 finished with value: -38094.95893125291 and parameters: {'model__n_estimators': 350, 'model__max_depth': 7, 'model__min_samples_split': 11, 'model__min_samples_leaf': 8, 'model__criterion': 'squared_error', 'model__max_features': 'sqrt', 'model__min_impurity_decrease': 0.23, 'model__max_samples': 0.8145512328599559}. Best is trial 0 with value: -36120.24060736828.
[I 2025-03-27 11:55:13,966] Trial 3 finished with value: -38777.06476564985 and parameters: {'model__n_estimators': 300, 'model__max_depth': 11, 'model__min_samples_split': 12, 'model__min_samples_leaf': 9, 'model__criterion': 'squared_error', 'model__max_features': 'sqrt', 'model__min_impurity_decrease': 0.46, 'model__max_samples': 0.6789951285473868}. Best is trial 0 with value: -36120.24060736828.
[I 2025-03-27 11:55:15,741] Trial 4 finished with value: -33297.41443086666 and parameters: {'model__n_estimators': 250, 'model__max_depth': 11, 'model__min_samples_split': 5, 'model__min_samples_leaf': 5, 'model__criterion': 'friedman_mse', 'model__max_features': None, 'model__min_impurity_decrease': 0.39, 'model__max_samples': 0.5476424157409567}. Best is trial 4 with value: -33297.41443086666.
[I 2025-03-27 11:55:17,476] Trial 5 finished with value: -37474.18864279444 and parameters: {'model__n_estimators': 250, 'model__max_depth': 14, 'model__min_samples_split': 4, 'model__min_samples_leaf': 6, 'model__criterion': 'friedman_mse', 'model__max_features': 'sqrt', 'model__min_impurity_decrease': 0.47000000000000003, 'model__max_samples': 0.5378415045501383}. Best is trial 4 with value: -33297.41443086666.
[I 2025-03-27 11:55:19,255] Trial 6 finished with value: -32145.728974372254 and parameters: {'model__n_estimators': 250, 'model__max_depth': 14, 'model__min_samples_split': 6, 'model__min_samples_leaf': 1, 'model__criterion': 'friedman_mse', 'model__max_features': None, 'model__min_impurity_decrease': 0.04, 'model__max_samples': 0.5499841742828928}. Best is trial 6 with value: -32145.728974372254.
[I 2025-03-27 11:55:19,906] Trial 7 finished with value: -40366.31495235264 and parameters: {'model__n_estimators': 50, 'model__max_depth': 10, 'model__min_samples_split': 6, 'model__min_samples_leaf': 5, 'model__criterion': 'squared_error', 'model__max_features': 'log2', 'model__min_impurity_decrease': 0.29, 'model__max_samples': 0.8355538352829297}. Best is trial 6 with value: -32145.728974372254.
[I 2025-03-27 11:55:20,865] Trial 8 finished with value: -44818.00052270107 and parameters: {'model__n_estimators': 100, 'model__max_depth': 9, 'model__min_samples_split': 17, 'model__min_samples_leaf': 7, 'model__criterion': 'absolute_error', 'model__max_features': 'log2', 'model__min_impurity_decrease': 0.35000000000000003, 'model__max_samples': 0.8342282217075911}. Best is trial 6 with value: -32145.728974372254.
[I 2025-03-27 11:55:23,938] Trial 9 finished with value: -42702.66284832203 and parameters: {'model__n_estimators': 500, 'model__max_depth': 15, 'model__min_samples_split': 6, 'model__min_samples_leaf': 10, 'model__criterion': 'squared_error', 'model__max_features': 'log2', 'model__min_impurity_decrease': 0.4, 'model__max_samples': 0.9572046439258426}. Best is trial 6 with value: -32145.728974372254.
RMSE: 32145.728974372254

Случайный лес справился хуже градиентного бустинга.

И наш победитель - Лёгкий градиентный бустинг. Далее на очереди итоговое тестирование.

На этом завершается обучение моделей. В рамках обучения, мы:

  • Подготовили пайплайны с препроцессингом для трёх моделей машинного обучения - SVM, LGBM и RandomForest
  • Провели подбор гиперпараметров на OptunaSearchCV
  • Провели кросс-валидацию моделей с лучшими параметрами и выявили лучшую модель - Лёгкий градиентный бустинг.

Тестирование качества лучшей модели¶

Выбор лучшей модели был сделан, а значит больше нам не пригодятся по-отдельности тренировочные и валидационные данные. Можем их все скормить лучшей модели.

In [13]:
lgbm_cv.best_estimator_.fit(X_train, y_train)
Out[13]:
Pipeline(steps=[('preprocess',
                 ColumnTransformer(remainder='passthrough',
                                   transformers=[('ohe',
                                                  Pipeline(steps=[('impute',
                                                                   SimpleImputer(strategy='most_frequent')),
                                                                  ('encode',
                                                                   OneHotEncoder(drop='first',
                                                                                 handle_unknown='ignore',
                                                                                 sparse_output=False))]),
                                                  Index(['MSZoning', 'Alley', 'LotShape', 'LandContour', 'LotConfig',
       'Neighborhood', 'Condition1', 'Condition2', '...
       'Fireplaces', 'GarageYrBlt', 'GarageCars', 'GarageArea', 'WoodDeckSF',
       'OpenPorchSF', 'PoolArea', 'MoSold'],
      dtype='object'))])),
                ('model',
                 LGBMRegressor(colsample_bytree=0.6689929656509956,
                               learning_rate=0.08794560715288294, max_depth=5,
                               min_child_samples=30, n_estimators=400,
                               num_leaves=133, objective='poisson',
                               random_state=10, reg_alpha=0.9148078283231919,
                               reg_lambda=0.3579902570947734, verbose=-1))])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('preprocess',
                 ColumnTransformer(remainder='passthrough',
                                   transformers=[('ohe',
                                                  Pipeline(steps=[('impute',
                                                                   SimpleImputer(strategy='most_frequent')),
                                                                  ('encode',
                                                                   OneHotEncoder(drop='first',
                                                                                 handle_unknown='ignore',
                                                                                 sparse_output=False))]),
                                                  Index(['MSZoning', 'Alley', 'LotShape', 'LandContour', 'LotConfig',
       'Neighborhood', 'Condition1', 'Condition2', '...
       'Fireplaces', 'GarageYrBlt', 'GarageCars', 'GarageArea', 'WoodDeckSF',
       'OpenPorchSF', 'PoolArea', 'MoSold'],
      dtype='object'))])),
                ('model',
                 LGBMRegressor(colsample_bytree=0.6689929656509956,
                               learning_rate=0.08794560715288294, max_depth=5,
                               min_child_samples=30, n_estimators=400,
                               num_leaves=133, objective='poisson',
                               random_state=10, reg_alpha=0.9148078283231919,
                               reg_lambda=0.3579902570947734, verbose=-1))])
ColumnTransformer(remainder='passthrough',
                  transformers=[('ohe',
                                 Pipeline(steps=[('impute',
                                                  SimpleImputer(strategy='most_frequent')),
                                                 ('encode',
                                                  OneHotEncoder(drop='first',
                                                                handle_unknown='ignore',
                                                                sparse_output=False))]),
                                 Index(['MSZoning', 'Alley', 'LotShape', 'LandContour', 'LotConfig',
       'Neighborhood', 'Condition1', 'Condition2', 'BldgType', 'HouseStyle',
       'RoofSt...
                                 Index(['LotFrontage', 'LotArea', 'OverallQual', 'OverallCond', 'YearBuilt',
       'YearRemodAdd', 'MasVnrArea', 'BsmtFinSF1', 'BsmtUnfSF', 'TotalBsmtSF',
       '1stFlrSF', '2ndFlrSF', 'GrLivArea', 'BsmtFullBath', 'FullBath',
       'HalfBath', 'BedroomAbvGr', 'KitchenAbvGr', 'TotRmsAbvGrd',
       'Fireplaces', 'GarageYrBlt', 'GarageCars', 'GarageArea', 'WoodDeckSF',
       'OpenPorchSF', 'PoolArea', 'MoSold'],
      dtype='object'))])
Index(['MSZoning', 'Alley', 'LotShape', 'LandContour', 'LotConfig',
       'Neighborhood', 'Condition1', 'Condition2', 'BldgType', 'HouseStyle',
       'RoofStyle', 'RoofMatl', 'Exterior1st', 'Exterior2nd', 'MasVnrType',
       'ExterQual', 'Foundation', 'BsmtQual', 'BsmtCond', 'BsmtExposure',
       'BsmtFinType1', 'BsmtFinType2', 'HeatingQC', 'CentralAir', 'Electrical',
       'KitchenQual', 'FireplaceQu', 'GarageType', 'GarageFinish',
       'GarageQual', 'GarageCond', 'PavedDrive', 'PoolQC', 'Fence', 'SaleType',
       'SaleCondition'],
      dtype='object')
SimpleImputer(strategy='most_frequent')
OneHotEncoder(drop='first', handle_unknown='ignore', sparse_output=False)
Index(['LotFrontage', 'LotArea', 'OverallQual', 'OverallCond', 'YearBuilt',
       'YearRemodAdd', 'MasVnrArea', 'BsmtFinSF1', 'BsmtUnfSF', 'TotalBsmtSF',
       '1stFlrSF', '2ndFlrSF', 'GrLivArea', 'BsmtFullBath', 'FullBath',
       'HalfBath', 'BedroomAbvGr', 'KitchenAbvGr', 'TotRmsAbvGrd',
       'Fireplaces', 'GarageYrBlt', 'GarageCars', 'GarageArea', 'WoodDeckSF',
       'OpenPorchSF', 'PoolArea', 'MoSold'],
      dtype='object')
SimpleImputer(strategy='median')
StandardScaler()
[]
passthrough
LGBMRegressor(colsample_bytree=0.6689929656509956,
              learning_rate=0.08794560715288294, max_depth=5,
              min_child_samples=30, n_estimators=400, num_leaves=133,
              objective='poisson', random_state=10,
              reg_alpha=0.9148078283231919, reg_lambda=0.3579902570947734,
              verbose=-1)

А теперь приступим к финальным тестам. Начнём с простого просмотра метрик MAE, RMSE, R2 и MAPE.

In [14]:
y_pred = lgbm_cv.best_estimator_.predict(X_test)

metrics = {
    "MAE": f"{mean_absolute_error(y_test, y_pred):.3f}",
    "RMSE": f"{np.sqrt(mean_squared_error(y_test, y_pred)):.3f}",
    "R²": f"{r2_score(y_test, y_pred):.3f}",
    "MAPE": f"{np.mean(np.abs((y_test - y_pred) / y_test)) * 100:.2f}%"
}

pd.DataFrame.from_dict(metrics, orient='index', columns=['Value'])
Out[14]:
Value
MAE 16950.581
RMSE 26473.616
R² 0.880
MAPE 10.12%

Заметьте, MAE значительно меньше RMSE. А метрика RMSE более чувствительна к выбросам. Эта особенность говорит нам о наличии аномально больших разрывов между реальными значениями и предсказанными.
Остальные метрики показывают вполне оптимистичное значение.

Вернёмся к аномалиям. Лучший способ наглядно за ними проследить - это построить график. На нём они от нас не убегут. Сопоставим реальные значения с предсказанными используя regplot из seaborn.

In [15]:
plt.figure(figsize=(10, 6))
sns.regplot(x=y_test, y=y_pred, scatter_kws={'alpha':0.4, 'color':'#1f77b4'}, line_kws={'color':'red'})
plt.plot([min(y_test), max(y_test)], [min(y_test), max(y_test)], '--', color='gray')
plt.title(f'LGBM: Истинное vs Предсказанное', pad=20)
plt.xlabel('Истинные значения', labelpad=10)
plt.ylabel('Предсказанные значения', labelpad=10)
plt.grid(alpha=0.3)
plt.show()
No description has been provided for this image

А вот и причина высокого RMSE. Модель сильно и систематически занижает стоимость дорогого жилья. Предсказания по жилью низкого и среднего ценового сегмента близки к эталону. Скорее всего причина в недостатке записей с высокой стоимостью жилья. Модели почти не на чем было учиться.

На всякий случай взглянем также на это с другой стороны. Посмотрим распределение ошибок на гистограмме.

In [16]:
plt.figure(figsize=(10, 5))
errors = y_test - y_pred
sns.histplot(errors, kde=True, bins=30, color='#2ca02c')
plt.title('Распределение ошибок предсказания', pad=15)
plt.xlabel('Ошибка (Истинное - Предсказанное)', labelpad=10)
plt.ylabel('Частота', labelpad=10)
plt.axvline(x=0, color='red', linestyle='--')
plt.grid(alpha=0.3)
plt.show()
No description has been provided for this image

Распределение близко к нормальному, здесь проблем нет.

Мы проверили качество модели, но пока не рассматривали из чего оно формируется. Проверим какие признаки вносят наибольший вклад в решения модели.
Для интерпритации используем shap-графики. На графике beeswarm рассмотрим общую важность признаков, а на графике waterfall проследим за решением на основе одной конкретной записи.

In [17]:
explainer = shap.TreeExplainer(lgbm_cv.best_estimator_.named_steps['model'])
preprocessor.fit(X_train)
X_test_transformed = pd.DataFrame(
    preprocessor.transform(X_test),
    columns=preprocessor.get_feature_names_out(),
    index=X_test.index 
)


shap_values = explainer.shap_values(X_test_transformed)

plt.figure(figsize=(10, 6))
shap.summary_plot(shap_values, X_test_transformed, plot_type="dot", show=False, feature_names=preprocessor.get_feature_names_out())
plt.title('Важность признаков SHAP', pad=20)
plt.tight_layout()
plt.show()

plt.title('Влияние признаков на конкретную запись', pad=20)
shap.plots.waterfall(shap.Explanation(
    values=shap_values[42], 
    base_values=explainer.expected_value, 
    data=X_test_transformed.iloc[42], 
    feature_names=preprocessor.get_feature_names_out()
))
No description has been provided for this image
No description has been provided for this image

Наибольший вклад вносят показатели площади жилья и десятибальная оценка общего качества, что звучит логично. Чем выше показатели обоих признаков - тем модель более склонна отнести жильё к дорогому.

На этом финальное тестирование завершается. Итого, мы:

  • Обучили модель на всей тренировочной выборке;
  • Вывели показатели четырёх метрик на тестовых данных: MAE, RMSE, R2 и MAPE. По метрикам можно сделать вывод, что модель хорошо справляется со своими обязанностями.
  • Построили график соотношения истинных и предсказанных значений тестовых данных, выявили аномальные значения ошибок и систематическое занижение предсказанных значений на дорогом жилье.
  • Построили гистограмму распределения ошибок предсказания, убедились в нормальности распределения.
  • Построили два SHAP-графика для оценки важности признаков и выявили наиболее ценные признаки - GrLivArea и OverallQual

Итоговый вывод¶

В рамках разведочного анализа и предобработки, мы:

  • Ознакомились с данными, выявили потенциальные проблемы и малоинформативные признаки;
  • Подготовили инструменты автоматической предобработки данных.

В рамках обучения моделей, мы:

  • Обучили, подобрали параметры и провели кросс-валидацию для трёх разнообразных моделей машинного обучения;
  • Выявили лучшую модель - Лёгкий градиентный бустинг.

В рамках итогового тестирования, мы:

  • Ознакомились с метриками MAE, RMSE, R2 и MAPE;
  • Выявили проблему недостатка данных о дорогом жилье в выборке;
  • Выявили наиболее информативные признаки - размер жилья и общая оценка качества;

Рекомендации заказчику:

  • Дополнить данные записями о более разнообразном по стоимости жилье для улучшения качества предсказаний.
In [ ]: